﻿// ReSharper disable PossibleNullReferenceException
// ReSharper disable IdentifierTypo

using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;

namespace KinonekoSoftware.Studio.Resources.Controls
{



    /// <summary>
    /// Zoom control.
    /// </summary>
    [TemplatePart(Name = PartPresenter, Type = typeof(ZoomContentPresenter))]
    public sealed class ZoomControl : ContentControl
    {
        [System.Diagnostics.CodeAnalysis.NotNull]
        private const string PartPresenter = "PART_Presenter";

        private Point                _mouseDownPos;
        private ZoomContentPresenter _presenter;
        private ScaleTransform       _scaleTransform;
        private Vector               _startTranslate;
        private TransformGroup       _transformGroup;
        private TranslateTransform   _translateTransform;
        private bool                 _isZooming;

        /// <summary/>
        static ZoomControl()
        {
            TranslateXProperty.Changed.Subscribe(OnTranslateXPropertyChanged);
            TranslateYProperty.Changed.Subscribe(OnTranslateYPropertyChanged);
            ZoomProperty.Changed.Subscribe(OnZoomPropertyChanged);
            ModeProperty.Changed.Subscribe(OnModePropertyChanged);
        }

        /// <summary/>
        public ZoomControl()
        {
            PointerWheelChanged += OnZoomControlMouseWheel;
            PointerPressed      += OnZoomControlMouseDown;
            PointerReleased     += OnZoomControlMouseUp;
        }

        public IBrush ZoomBoxBackground
        {
            get => GetValue(ZoomBoxBackgroundProperty);
            set => SetValue(ZoomBoxBackgroundProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<IBrush> ZoomBoxBackgroundProperty
            = AvaloniaProperty.Register<ZoomControl, IBrush>(nameof(ZoomBoxBackground));

        public IBrush ZoomBoxBorderBrush
        {
            get => GetValue(ZoomBoxBorderBrushProperty);
            set => SetValue(ZoomBoxBorderBrushProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<IBrush> ZoomBoxBorderBrushProperty
            = AvaloniaProperty.Register<ZoomControl, IBrush>(nameof(ZoomBoxBorderBrush));

        public Thickness ZoomBoxBorderThickness
        {
            get => GetValue(ZoomBoxBorderThicknessProperty);
            set => SetValue(ZoomBoxBorderThicknessProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<Thickness> ZoomBoxBorderThicknessProperty
            = AvaloniaProperty.Register<ZoomControl, Thickness>(nameof(ZoomBoxBorderThickness));

        public double ZoomBoxOpacity
        {
            get => GetValue(ZoomBoxOpacityProperty);
            set => SetValue(ZoomBoxOpacityProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> ZoomBoxOpacityProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(ZoomBoxOpacity), 
                                                             0.5);

        public Rect ZoomBox
        {
            get => GetValue(ZoomBoxProperty);
            set => SetValue(ZoomBoxProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<Rect> ZoomBoxProperty
            = AvaloniaProperty.Register<ZoomControl, Rect>(
                                                           nameof(ZoomBox));

        public Point OrigoPosition => new Point(Bounds.Width / 2.0, Bounds.Height / 2.0);

        public double TranslateX
        {
            get => GetValue(TranslateXProperty);
            set
            {
                //BeginAnimation(TranslateXProperty, null);
                SetValue(TranslateXProperty, value);
            }
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> TranslateXProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(TranslateX), 
                                                             0.0,
                                                             false,
                                                             BindingMode.OneWay,
                                                             null,
                                                             OnTranslateXCoerce);

        public double TranslateY
        {
            get => GetValue(TranslateYProperty);
            set
            {
                //BeginAnimation(TranslateYProperty, null);
                SetValue(TranslateYProperty, value);
            }
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> TranslateYProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(TranslateY),
                                                             0.0,
                                                             false,
                                                             BindingMode.OneWay,
                                                             null,
                                                             OnTranslateYCoerce);

        public TimeSpan AnimationLength
        {
            get => GetValue(AnimationLengthProperty);
            set => SetValue(AnimationLengthProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<TimeSpan> AnimationLengthProperty
            = AvaloniaProperty.Register<ZoomControl, TimeSpan>(
                                                               nameof(AnimationLength),
                                                               TimeSpan.FromMilliseconds(500.0));

        public double MinZoom
        {
            get => GetValue(MinZoomProperty);
            set => SetValue(MinZoomProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> MinZoomProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(MinZoom), 0.01);

        public double MaxZoom
        {
            get => GetValue(MaxZoomProperty);
            set => SetValue(MaxZoomProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> MaxZoomProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(MaxZoom), 100.0);

        public double MaxZoomDelta
        {
            get => GetValue(MaxZoomDeltaProperty);
            set => SetValue(MaxZoomDeltaProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> MaxZoomDeltaProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(MaxZoomDelta),5.0);

        public double ZoomDeltaMultiplier
        {
            get => GetValue(ZoomDeltaMultiplierProperty);
            set => SetValue(ZoomDeltaMultiplierProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> ZoomDeltaMultiplierProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(ZoomDeltaMultiplier),100.0);

        public double Zoom
        {
            get => GetValue(ZoomProperty);
            set
            {
                if (Math.Abs(GetValue(ZoomProperty) - value) < double.Epsilon)
                    return;

                // BeginAnimation(ZoomProperty, null);
                SetValue(ZoomProperty, value);
            }
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<double> ZoomProperty
            = AvaloniaProperty.Register<ZoomControl, double>(
                                                             nameof(Zoom), 1.0);

        private ZoomContentPresenter ContentPresenter
        {
            get => _presenter;
            set
            {
                _presenter = value;
                if (_presenter is null)
                    return;

                _transformGroup     = new TransformGroup();
                _scaleTransform     = new ScaleTransform();
                _translateTransform = new TranslateTransform();
                _transformGroup.Children.Add(_scaleTransform);
                _transformGroup.Children.Add(_translateTransform);
                _presenter.RenderTransform       = _transformGroup;
                _presenter.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
            }
        }

        public ZoomViewModifierMode ModifierMode
        {
            get => GetValue(ModifierModeProperty);
            set => SetValue(ModifierModeProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<ZoomViewModifierMode> ModifierModeProperty
            = AvaloniaProperty.Register<ZoomControl, ZoomViewModifierMode>(
                                                                           nameof(ModifierMode));

        public ZoomControlModes Mode
        {
            get => GetValue(ModeProperty);
            set => SetValue(ModeProperty, value);
        }

        [System.Diagnostics.CodeAnalysis.NotNull]
        public static readonly StyledProperty<ZoomControlModes> ModeProperty
            = AvaloniaProperty.Register<ZoomControl, ZoomControlModes>(
                                                                       nameof(Mode), 
                                                                       ZoomControlModes.Custom);

        private static void OnModePropertyChanged(
            [System.Diagnostics.CodeAnalysis.NotNull]
            AvaloniaPropertyChangedEventArgs<ZoomControlModes> args)
        {
            var zoomControl = (ZoomControl)args.Sender;
            switch (args.GetNewValue<ZoomControlModes>())
            {
                case ZoomControlModes.Fill:
                    zoomControl.DoZoomToFill();
                    break;

                case ZoomControlModes.Original:
                    zoomControl.DoZoomToOriginal();
                    break;

                case ZoomControlModes.Custom:
                    break;

                default:
                    throw new NotSupportedException();
            }
        }

        [Pure]
        private static double OnTranslateXCoerce(AvaloniaObject d, double basevalue)
        {
            var zoomControl = (ZoomControl)d;
            return zoomControl.GetCoercedTranslateX(basevalue);
        }

        [Pure]
        private double GetCoercedTranslateX(double baseValue)
        {
            return _presenter is null ? 0.0 : baseValue;
        }

        [Pure]
        private static double OnTranslateYCoerce([System.Diagnostics.CodeAnalysis.NotNull] AvaloniaObject d, double basevalue)
        {
            var zoomControl = (ZoomControl)d;
            return zoomControl.GetCoercedTranslateY(basevalue);
        }

        [Pure]
        private double GetCoercedTranslateY(double baseValue)
        {
            return _presenter is null ? 0.0 : baseValue;
        }

        private void OnZoomControlMouseUp([System.Diagnostics.CodeAnalysis.NotNull] object sender, [System.Diagnostics.CodeAnalysis.NotNull] PointerReleasedEventArgs args)
        {
            switch (ModifierMode)
            {
                case ZoomViewModifierMode.None:
                    break;

                case ZoomViewModifierMode.Pan:
                case ZoomViewModifierMode.ZoomIn:
                case ZoomViewModifierMode.ZoomOut:
                    ModifierMode     =  ZoomViewModifierMode.None;
                    PointerMoved -= OnZoomControlPointerMove;
                    break;

                case ZoomViewModifierMode.ZoomBox:
                    ZoomTo(ZoomBox);
                    goto case ZoomViewModifierMode.Pan;

                default:
                    throw new NotSupportedException();
            }
        }

        public void ZoomTo(Rect rect)
        {
            DoZoom(
                   Math.Min(Bounds.Width / rect.Width, Bounds.Height / rect.Height),
                   OrigoPosition,
                   new Point(rect.X + rect.Width / 2.0, rect.Y + rect.Height / 2.0),
                   OrigoPosition);
            ZoomBox = new Rect();
        }

        private void OnZoomControlPointerMove([System.Diagnostics.CodeAnalysis.NotNull] object sender, [System.Diagnostics.CodeAnalysis.NotNull] PointerEventArgs args)
        {
            switch (ModifierMode)
            {
                case ZoomViewModifierMode.None:
                case ZoomViewModifierMode.ZoomIn:
                case ZoomViewModifierMode.ZoomOut:
                    break;

                case ZoomViewModifierMode.Pan:
                    var vector = _startTranslate + (args.GetPosition(this) - _mouseDownPos);
                    TranslateX = vector.X;
                    TranslateY = vector.Y;
                    break;

                case ZoomViewModifierMode.ZoomBox:
                    var position = args.GetPosition(this);
                    ZoomBox = new Rect(
                                       Math.Min(_mouseDownPos.X, position.X),
                                       Math.Min(_mouseDownPos.Y, position.Y),
                                       Math.Abs(_mouseDownPos.X - position.X),
                                       Math.Abs(_mouseDownPos.Y - position.Y));
                    break;

                default:
                    throw new NotSupportedException();
            }
        }

        private void OnZoomControlMouseDown([System.Diagnostics.CodeAnalysis.NotNull] object sender, [System.Diagnostics.CodeAnalysis.NotNull] PointerPressedEventArgs args) => OnMouseDown(args, false);


        private void OnMouseDown([System.Diagnostics.CodeAnalysis.NotNull] PointerPressedEventArgs args, bool isPreview)
        {
            if (ModifierMode != ZoomViewModifierMode.None)
                return;

            switch (args.KeyModifiers)
            {
                case KeyModifiers.None:
                    if (!isPreview)
                    {
                        ModifierMode = ZoomViewModifierMode.Pan;
                        goto case KeyModifiers.Control;
                    }
                    else
                        goto case KeyModifiers.Control;

                case KeyModifiers.Alt:
                    ModifierMode = ZoomViewModifierMode.ZoomBox;
                    goto case KeyModifiers.Control;

                case KeyModifiers.Control:
                case KeyModifiers.Meta:
                    if (ModifierMode == ZoomViewModifierMode.None)
                        break;

                    _mouseDownPos   = args.GetPosition(this);
                    _startTranslate = new Vector(TranslateX, TranslateY);
                    PointerMoved += OnZoomControlPointerMove;
                    break;

                case KeyModifiers.Shift:
                    ModifierMode = ZoomViewModifierMode.Pan;
                    goto case KeyModifiers.Control;
            }
        }

        private static void OnTranslateXPropertyChanged([System.Diagnostics.CodeAnalysis.NotNull] AvaloniaPropertyChangedEventArgs<double> args)
        {
            var zoomControl = (ZoomControl)args.Sender;
            if (zoomControl._translateTransform is null)
                return;

            zoomControl._translateTransform.X = args.GetNewValue<double>();
            if (zoomControl._isZooming)
                return;

            zoomControl.Mode = ZoomControlModes.Custom;
        }

        private static void OnTranslateYPropertyChanged(
            [System.Diagnostics.CodeAnalysis.NotNull]
            AvaloniaPropertyChangedEventArgs<double> args)
        {
            var zoomControl = (ZoomControl)args.Sender;
            if (zoomControl._translateTransform is null)
                return;

            zoomControl._translateTransform.Y = args.GetNewValue<double>();
            if (zoomControl._isZooming)
                return;

            zoomControl.Mode = ZoomControlModes.Custom;
        }

        private static void OnZoomPropertyChanged(AvaloniaPropertyChangedEventArgs<double> args)
        {
            var zoomControl = (ZoomControl)args.Sender;
            if (zoomControl._scaleTransform is null)
                return;

            var newValue = args.GetNewValue<double>();
            zoomControl._scaleTransform.ScaleX = newValue;
            zoomControl._scaleTransform.ScaleY = newValue;
            if (zoomControl._isZooming)
                return;

            var num = newValue / args.GetOldValue<double>();
            zoomControl.TranslateX *= num;
            zoomControl.TranslateY *= num;
            zoomControl.Mode       =  ZoomControlModes.Custom;
        }

        private void OnZoomControlMouseWheel([System.Diagnostics.CodeAnalysis.NotNull] object sender, [System.Diagnostics.CodeAnalysis.NotNull] PointerWheelEventArgs args)
        {
            if ((args.KeyModifiers & KeyModifiers.Control) <= KeyModifiers.None || ModifierMode != ZoomViewModifierMode.None)
                return;

            args.Handled = true;
            var delta         = Math.Min(args.Delta.X, args.Delta.Y);
            var origoPosition = new Point(Bounds.Width / 2.0, Bounds.Height / 2.0);
            var position      = args.GetPosition(this);
            DoZoom(
                   Math.Max(1.0 / MaxZoomDelta, Math.Min(MaxZoomDelta, delta / 10000.0 * ZoomDeltaMultiplier + 1.0)),
                   origoPosition,
                   position,
                   position);
        }

        private void DoZoom(
            double deltaZoom,
            Point origoPosition,
            Point startHandlePosition,
            Point targetHandlePosition)
        {
            var zoom              = Zoom;
            var num               = Math.Max(MinZoom, Math.Min(MaxZoom, zoom * deltaZoom));
            var vector1           = new Vector(TranslateX, TranslateY);
            var vector2           = startHandlePosition  - origoPosition;
            var vector3           = targetHandlePosition - origoPosition - ((vector2 - vector1) / zoom * num + vector1);
            var coercedTranslateX = GetCoercedTranslateX(TranslateX + vector3.X);
            var coercedTranslateY = GetCoercedTranslateY(TranslateY + vector3.Y);
            DoZoomAnimation(num, coercedTranslateX, coercedTranslateY);
            Mode = ZoomControlModes.Custom;
        }

        private void DoZoomAnimation(double targetZoom, double transformX, double transformY)
        {
            _isZooming = true;
            //
            // var duration = new Duration(AnimationLength);
            // StartAnimation(TranslateXProperty, transformX, duration);
            // StartAnimation(TranslateYProperty, transformY, duration);
            // StartAnimation(ZoomProperty, targetZoom, duration);
        }



        public void ZoomToOriginal() => Mode = ZoomControlModes.Original;

        private void DoZoomToOriginal()
        {
            if (_presenter is null)
                return;

            var initialTranslate = GetInitialTranslate();
            DoZoomAnimation(1.0, initialTranslate.X, initialTranslate.Y);
        }

        [Pure]
        private Vector GetInitialTranslate()
        {
            return _presenter is null
                ? default
                : new Vector(
                             -(_presenter.ContentSize.Width  - _presenter.DesiredSize.Width)  / 2.0,
                             -(_presenter.ContentSize.Height - _presenter.DesiredSize.Height) / 2.0);
        }

        public void ZoomToFill() => Mode = ZoomControlModes.Fill;

        private void DoZoomToFill()
        {
            if (_presenter is null)
                return;

            var targetZoom = Math.Min(
                                      Bounds.Width  / _presenter.ContentSize.Width,
                                      Bounds.Height / _presenter.ContentSize.Height);

            var initialTranslate = GetInitialTranslate();
            DoZoomAnimation(targetZoom, initialTranslate.X * targetZoom, initialTranslate.Y * targetZoom);
        }

        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
        {
            
            base.OnApplyTemplate(e);
            
            
            ContentPresenter = e.NameScope.Get<ZoomContentPresenter>(PartPresenter);
            
            if (ContentPresenter != null)
            {
                ContentPresenter.SizeChanged += (_, _) =>
                {
                    if (Mode != ZoomControlModes.Fill)
                        return;
                    DoZoomToFill();
                };

                ContentPresenter.ContentSizeChanged += (_, _) =>
                {
                    if (Mode != ZoomControlModes.Fill)
                        return;
                    DoZoomToFill();
                };
            }

            ZoomToFill();
        }
    }
}